TechNote - QuickFiling Callbacks

August 30, 2026

 

Several commands (CopyFolderCommand, HashtagCommand, SearchCommand, SearchTitleCommand, EmbedCommand/EmbedSubpageCommand, and — via other pickers — ArchiveCommand, CheckUrlsCommand, RefreshPageLinksCommand, ImportEvernoteCommand) share a common shape: the user makes a choice in some modal or callback context, that choice kicks off non-trivial background work, and the work finishes with a follow-up report to the user. Three pieces work together to make this safe.

 

The pattern

 

Some of these commands drive OneNote's native QuickFiling picker (OneNote.SelectLocation(title, description, scope, callback)). OneNote shows its own picker UI and invokes callback from inside its own OnDialogClosed handling — OneNote's UI thread is blocked, waiting for the callback to return, for as long as the callback takes to run. Others use a modal setup dialog of their own instead. Either way, the same three components carry the work from there:

 

  1. Background execution. The real work runs inside ProgressDialog's Func<ProgressDialog, CancellationToken, Task> constructor, shown via RunModeless (inherited from MoreForm). Control returns to the caller — including OneNote's own UI thread, when the caller is a SelectLocation callback — almost immediately, instead of blocking it for the duration of the work.
  2. Correct ownership. OneNote.OwnerWindow always resolves through an internal GetTopLevelWindow() helper before wrapping a window handle, so every dialog it owns is owned by OneNote's actual top-level frame — never an inner pane.
  3. Decoupled reporting. The follow-up report (MoreMessageBox) is shown via HotkeyManager.InvokeOnMessageThread(() => MoreMessageBox.Show(...)), which marshals dialog creation onto a dedicated, persistent message-pump thread — instead of showing it synchronously from inside the RunModeless closed-action (ModelessClosed) handler, which itself runs nested inside a cross-thread Invoke that is unwinding the progress dialog's own Close().

 

Architecture

 

Architecture PlantUML (Extract)

 

Sequence

 

Sequence PlantUML (Extract)

 

Why ownership resolves to the top-level frame

 

OneNote.OwnerWindow can't simply wrap the COM Window.WindowHandle property, because that handle isn't guaranteed to be OneNote's top-level window — it can be an inner pane:

 

Structural PlantUML (Extract)

 

A dialog owned by a non-top-level HWND has no properly defined Win32 owner/owned relationship, so Windows' normal Z-order and activation protections for "owned" windows don't apply to it. GetTopLevelWindow() — a private helper on OneNote that walks GetParent until there's no parent left — is what OwnerWindow routes through so every dialog is anchored to the real frame instead.

 

Where this pattern is used today

 

Uses OneNote.SelectLocation (QuickFiling)

Uses its own setup dialog, same progress+report backbone

CopyFolderCommand, HashtagCommand, SearchCommand, SearchTitleCommand, EmbedCommand/EmbedSubpageCommand

ArchiveCommand, CheckUrlsCommand, RefreshPageLinksCommand, ImportEvernoteCommand

 

Building a new command on this pattern

 

  • Do the real work inside ProgressDialog's work delegate, run via RunModeless — never inline in a picker/dialog callback.
  • Let OneNote.OwnerWindow supply every dialog's owner. Don't source a raw COM window handle as an owner anywhere else without routing it through the same top-level resolution.
  • Show the follow-up report via HotkeyManager.InvokeOnMessageThread(() => MoreMessageBox.Show(...)) — never directly from the RunModeless closed-action/ModelessClosed handler.
  • For forced-activation needs, MoreForm.Elevate() is the proven AttachThreadInput/SetForegroundWindow/BringWindowToTop technique. It solves focus-priority contention; it isn't a substitute for correct ownership or threading if a dialog is being destroyed outright rather than merely losing focus.
  • OnActivated (synchronous) is the reliable hook for anything that must run before a window could be lost. OnShown is BeginInvoke-queued and can lose that race.
  • ProgressDialog.OnShown's Task.Factory.StartNew(async () => ...) currently has no .Unwrap() (or Task.Run) — a known sharp edge, worth tightening opportunistically if that code is touched again.
  • MoreMessageBox.Show(...) accepts optional widthScale/heightScale for report dialogs that need more room, e.g. a long partial-failure list.

 

History

 

This pattern was hardened after a regression surfaced during #2455 work, where CopyFolderCommand's report dialog appeared and was torn down within about 20ms — before a user could ever read it.

 

 

───────────────────────────────────────────────────────────────────────────────────────────────────

Architecture PlantUML (Refresh)

@startuml

skinparam componentStyle rectangle

actor "User" as user

component "OneNote UI Thread" as one {

[SelectLocation callback] as picker

}

component "Background Worker\n(ThreadPool)" as worker {

[ProgressDialog work delegate] as work

}

component "HotkeyManager\nMessage-Pump Thread" as pump {

[InvokeOnMessageThread] as invoke

}

user -> picker : chooses target

picker -> work : new ProgressDialog(...).RunModeless()\n[returns immediately]

work -> work : performs copy/import/refresh

work -> invoke : on completion,\nMoreMessageBox via InvokeOnMessageThread

invoke -> user : report dialog shown\n(owned by top-level frame)

note right of picker

Released almost instantly —

OneNote's UI thread is never

blocked for the duration of

the background work.

end note

note right of pump

A dedicated, already-running

Application.Run loop — dialog

creation never nests inside a

cross-thread Invoke/Close chain.

end note

@enduml

 

 

Sequence PlantUML (Refresh)

@startuml

skinparam sequenceMessageAlign center

participant "Background\nThread\n(copy/import work)" as BG

participant "ProgressDialog\n.Close()" as Close

participant "UI Thread\n(via Invoke)" as UI

participant "ReportResult\n(ModelessClosed\nhandler)" as Report

participant "HotkeyManager\nMessage Thread" as Hotkey

participant "MoreMessageBox\n.ShowDialog()" as Box

BG -> Close: finally { dialog.Close(); }

Close -> UI: Invoke((Action)Close)

activate UI

UI -> Report: OnFormClosed -> ModelessClosed

Report -> Hotkey: InvokeOnMessageThread(() => MoreMessageBox.Show(...))

Report -> UI: returns immediately

UI -> Close: Invoke() returns

deactivate UI

Close -> BG: dialog.Close() returns\n(background task finishes cleanly)

activate Hotkey

Hotkey -> Box: MoreMessageBox.Show(owner, ...)

Box -> Box: shown, activated, held

note right: owner is OneNote's top-level\nframe (OwnerWindow), so Windows'\nnormal owner/owned protection applies

deactivate Hotkey

@enduml

 

 

Structural PlantUML (Refresh)

@startsalt

{

{T

+ Framework::CFrame " - OneNote" (top-level window)

++ ... ribbon, tabs, navigation panes ...

++ OneNote::CWorkspace "" (inner pane - what Window.WindowHandle returns)

}

}

@endsalt

 

 

#omwiki #omdeveloper #omtechnote

 

© 2026 Steven M Cohn. All rights reserved.

Please consider a sponsorship or one-time donation to support ongoing development

 

Created with OneNote.